/*
 * Copyright 2014 Alan Burlison, alan@bleaklow.com.  All rights reserved.
 * Use is subject to license terms.  See LICENSE.txt for details.
 */

/*
 * Requires and is based in part on example code from:
 *     LUFA Library Copyright (C) Dean Camera, 2014.
 *     dean [at] fourwalledcubicle [dot] com www.lufa-lib.org
 * See License.txt in the LUFA distribution for LUFA license details.
 */

/*
 * LUFA-based CDC-ACM serial port support.  This extends the LUFA implementation
 * by adding basic line discipline handling - CR/NL handling, echoing, backspace
 * handling etc.
 */

#include <usb_serial.h>
#include <utils.h>
#include <clock.h>

/*
 * USB CDC device configuration and management.
 */

// Address of the device-to-host notification in endpoint.
#define NOTIFY_ENDPOINT (ENDPOINT_DIR_IN | 2)

// Address of the device-to-host data in endpoint.
#define TRANSMIT_ENDPOINT (ENDPOINT_DIR_IN | 3)

// Address of the host-to-device data out endpoint.
#define RECEIVE_ENDPOINT (ENDPOINT_DIR_OUT | 4)

// Size in bytes of the notification in endpoint.
#define NOTIFY_ENDPOINT_SIZE 8

// Size in bytes of the data in and out endpoints.
#define TXRX_ENDPOINT_SIZE 64

// Type define for the device configuration descriptor structure.
typedef struct {
    USB_Descriptor_Configuration_Header_t config;
    // CDC Control Interface.
    USB_Descriptor_Interface_t CCIInterface;
    USB_CDC_Descriptor_FunctionalHeader_t functionalHeader;
    USB_CDC_Descriptor_FunctionalACM_t functionalACM;
    USB_CDC_Descriptor_FunctionalUnion_t CDCFunctionalUnion;
    USB_Descriptor_Endpoint_t notificationEndpoint;
    // CDC Data Interface.
    USB_Descriptor_Interface_t DCIInterface;
    USB_Descriptor_Endpoint_t outputEndpoint;
    USB_Descriptor_Endpoint_t inputEndpoint;
} USB_Descriptor_Configuration_t;

// Device descriptor.
static const USB_Descriptor_Device_t PROGMEM deviceDescriptor = {
    .Header = {
        .Size = sizeof(USB_Descriptor_Device_t),
        .Type = DTYPE_Device
    },
    .USBSpecification = VERSION_BCD(1, 1, 0),
    .Class = CDC_CSCP_CDCClass,
    .SubClass = CDC_CSCP_NoSpecificSubclass,
    .Protocol = CDC_CSCP_NoSpecificProtocol,
    .Endpoint0Size = FIXED_CONTROL_ENDPOINT_SIZE,
    .VendorID = 0x0430,
    .ProductID = 0x0001,
    .ReleaseNumber = VERSION_BCD(0, 0, 1),
    .ManufacturerStrIndex = 0x01,
    .ProductStrIndex = 0x02,
    .SerialNumStrIndex = USE_INTERNAL_SERIAL,
    .NumberOfConfigurations = FIXED_NUM_CONFIGURATIONS
};

// Configuration descriptor.
static const USB_Descriptor_Configuration_t PROGMEM configurationDescriptor = {
    .config = {
        .Header = {
            .Size = sizeof(USB_Descriptor_Configuration_Header_t),
            .Type = DTYPE_Configuration
        },
        .TotalConfigurationSize = sizeof(USB_Descriptor_Configuration_t),
        .TotalInterfaces = 2,
        .ConfigurationNumber = 1,
        .ConfigurationStrIndex = NO_DESCRIPTOR,
        .ConfigAttributes =
          USB_CONFIG_ATTR_RESERVED | USB_CONFIG_ATTR_SELFPOWERED,
        .MaxPowerConsumption = USB_CONFIG_POWER_MA(100)
    },
    .CCIInterface = {
        .Header = {
            .Size = sizeof(USB_Descriptor_Interface_t),
            .Type = DTYPE_Interface
        },
        .InterfaceNumber = 0,
        .AlternateSetting = 0,
        .TotalEndpoints = 1,
        .Class = CDC_CSCP_CDCClass,
        .SubClass = CDC_CSCP_ACMSubclass,
        .Protocol = CDC_CSCP_ATCommandProtocol,
        .InterfaceStrIndex = NO_DESCRIPTOR
    },
    .functionalHeader = {
        .Header = {
            .Size = sizeof(USB_CDC_Descriptor_FunctionalHeader_t),
            .Type = DTYPE_CSInterface
        },
        .Subtype = CDC_DSUBTYPE_CSInterface_Header,
        .CDCSpecification = VERSION_BCD(1, 1, 0),
    },
    .functionalACM = {
        .Header = {
            .Size = sizeof(USB_CDC_Descriptor_FunctionalACM_t),
            .Type = DTYPE_CSInterface
        },
        .Subtype = CDC_DSUBTYPE_CSInterface_ACM,
        .Capabilities = 0x06,
    },
    .CDCFunctionalUnion = {
        .Header = {
            .Size = sizeof(USB_CDC_Descriptor_FunctionalUnion_t),
            .Type = DTYPE_CSInterface
        },
        .Subtype = CDC_DSUBTYPE_CSInterface_Union,
        .MasterInterfaceNumber = 0,
        .SlaveInterfaceNumber = 1,
    },
    .notificationEndpoint = {
        .Header = {
            .Size = sizeof(USB_Descriptor_Endpoint_t),
            .Type = DTYPE_Endpoint
        },
        .EndpointAddress = NOTIFY_ENDPOINT,
        .Attributes =
          EP_TYPE_INTERRUPT | ENDPOINT_ATTR_NO_SYNC | ENDPOINT_USAGE_DATA,
        .EndpointSize = NOTIFY_ENDPOINT_SIZE,
        .PollingIntervalMS = 0xFF
    },
    .DCIInterface = {
        .Header = {
            .Size = sizeof(USB_Descriptor_Interface_t),
            .Type = DTYPE_Interface
        },
        .InterfaceNumber = 1,
        .AlternateSetting = 0,
        .TotalEndpoints = 2,
        .Class = CDC_CSCP_CDCDataClass,
        .SubClass = CDC_CSCP_NoDataSubclass,
        .Protocol = CDC_CSCP_NoDataProtocol,
        .InterfaceStrIndex = NO_DESCRIPTOR
    },
    .outputEndpoint = {
        .Header = {
            .Size = sizeof(USB_Descriptor_Endpoint_t),
            .Type = DTYPE_Endpoint
        },
        .EndpointAddress = RECEIVE_ENDPOINT,
        .Attributes =
          (EP_TYPE_BULK | ENDPOINT_ATTR_NO_SYNC | ENDPOINT_USAGE_DATA),
        .EndpointSize = TXRX_ENDPOINT_SIZE,
        .PollingIntervalMS = 0x05
    },
    .inputEndpoint = {
        .Header = {
            .Size = sizeof(USB_Descriptor_Endpoint_t),
            .Type = DTYPE_Endpoint
        },
        .EndpointAddress = TRANSMIT_ENDPOINT,
        .Attributes =
          (EP_TYPE_BULK | ENDPOINT_ATTR_NO_SYNC | ENDPOINT_USAGE_DATA),
        .EndpointSize = TXRX_ENDPOINT_SIZE,
        .PollingIntervalMS = 0x05
    }
};

// Language descriptor.
static const USB_Descriptor_String_t PROGMEM languageString = {
    .Header = {
        .Size = USB_STRING_LEN(1),
        .Type = DTYPE_String
    },
    .UnicodeString = { LANGUAGE_ID_ENG }
};

// Manufacturer descriptor.
static const USB_Descriptor_String_t PROGMEM manufacturerString = {
    .Header = {
        .Size = USB_STRING_LEN(13),
        .Type = DTYPE_String
    },
    .UnicodeString = L"Alan Burlison"
};

// Product descriptor.
static const USB_Descriptor_String_t PROGMEM productString = {
    .Header = {
        .Size = USB_STRING_LEN(15),
        .Type = DTYPE_String
    },
    .UnicodeString = L"CDC Serial Port"
};

// LUFA CDC Class driver interface configuration and state information.
static USB_ClassInfo_CDC_Device_t serialDevice = {
    .Config = {
        .ControlInterfaceNumber = 0,
        .DataINEndpoint = {
            .Address = TRANSMIT_ENDPOINT,
            .Size = TXRX_ENDPOINT_SIZE,
            .Banks = 1,
        },
        .DataOUTEndpoint = {
            .Address = RECEIVE_ENDPOINT,
            .Size = TXRX_ENDPOINT_SIZE,
            .Banks = 1,
        },
        .NotificationEndpoint = {
            .Address = NOTIFY_ENDPOINT,
            .Size = NOTIFY_ENDPOINT_SIZE,
            .Banks = 1,
        },
    },
};

// Interface state.
#define CFGD 0x01               // Configured.
#define DTR  0x02               // DTR active.
#define UP   (CFGD | DTR)       // Configured and active.
static uint8_t state;

// Event handler for the library USB Configuration Changed event.  Call the
// appropriate LUFA routine and record success/failure.
void EVENT_USB_Device_ConfigurationChanged(void)
{
    if (CDC_Device_ConfigureEndpoints(&serialDevice)) {
        SET_MASK_HI(state, CFGD);
    } else {
        SET_MASK_LO(state, CFGD);
    }
}

// Event handler for the library USB Control Request event, call the appropriate
// LUFA library routine.
void EVENT_USB_Device_ControlRequest(void) {
    CDC_Device_ProcessControlRequest(&serialDevice);
}

// Event handler for the library USB disconnection event.  Record the event.
void EVENT_USB_Device_Disconnect(void)
{
    SET_MASK_LO(state, CFGD);
}

// Event handler for control line change events.  Record the DTR state.
void EVENT_CDC_Device_ControLineStateChanged(
  USB_ClassInfo_CDC_Device_t *const info)
{
    if (info->State.ControlLineStates.HostToDevice & CDC_CONTROL_LINE_OUT_DTR) {
        SET_MASK_HI(state, DTR);
    } else {
        SET_MASK_LO(state, DTR);
    }
}

// Return the requested device descriptor.
uint16_t CALLBACK_USB_GetDescriptor(const uint16_t value, const uint8_t index,
  const void **const address) {
    const uint8_t type = (value >> 8);
    const uint8_t number = (value & 0xFF);
    switch (type) {
        case DTYPE_Device:
            *address = &deviceDescriptor;
            return sizeof(USB_Descriptor_Device_t);
        case DTYPE_Configuration:
            *address = &configurationDescriptor;
            return sizeof(USB_Descriptor_Configuration_t);
        case DTYPE_String:
            switch (number) {
                case 0x00:
                    *address = &languageString;
                    return pgm_read_byte(&languageString.Header.Size);
                case 0x01:
                    *address = &manufacturerString;
                    return pgm_read_byte(&manufacturerString.Header.Size);
                case 0x02:
                    *address = &productString;
                    return pgm_read_byte(&productString.Header.Size);
            }
    }
    return 0;
}

/*
 * FILE buffer and line discipline handling.
 */

// Delete character.
#define DEL 0x7F

// IO buffer - circular queue.
typedef struct {
    char     *start;
    char     *end;
    char     *head;
    char     *tail;
    uint16_t len;
} iobuf_t;

// Forward declarations.
static int iobuf_putchar(char c, FILE *restrict file);
static int iobuf_getchar(FILE *restrict file);

// IO handling globals.
static uint32_t nextRun;
static uint8_t mode;
static uint8_t ilines;

// Output data structure.
static FILE output = {
  .put = &iobuf_putchar,
  .get = NULL,
  .flags = _FDEV_SETUP_WRITE,
  .udata = NULL
};

// Input data structures.
static char in[USB_IBUFSZ];
static iobuf_t ibuf = { in, in + USB_IBUFSZ - 1, in, in, 0 };
static FILE input = {
  .put = NULL,
  .get = &iobuf_getchar,
  .flags = _FDEV_SETUP_READ,
  .udata = &ibuf
};

// Enqueue a character.  Assumes there is enough space.
static void enqueue(char c, iobuf_t *restrict iobuf)
{
    *iobuf->head++ = c;
    if (iobuf->head > iobuf->end) {
        iobuf->head = iobuf->start;
    }
    iobuf->len++;
}

// Dequeue a character.  Assumes there is one available.
static int dequeue(iobuf_t *restrict iobuf)
{
    char c = *iobuf->tail++;
    if (iobuf->tail > iobuf->end) {
        iobuf->tail = iobuf->start;
    }
    iobuf->len--;
    return c;
}

// Run the LUFA CDC and USB tasks and buffer any pending input.  Performs input
// line discipline handling.
static void run(uint32_t now)
{
    // Run the CDC and USB tasks - write pending output, read input.
    CDC_Device_USBTask(&serialDevice);
    USB_USBTask();

    // Find out if there is any pending input.  If not, wait for 25 msec.
    uint16_t avail =  CDC_Device_BytesReceived(&serialDevice);
    if (! avail) {
        nextRun = now + 25;
        return;
    }

    // Text mode - echo and provide simple line editing.
    if (mode & USB_SERIAL_IN_TEXT) {
        while (avail--) {
            char c = CDC_Device_ReceiveByte(&serialDevice);
            switch (c) {
            // CR or NL - convert both to '\n' but echo "\r\n".
            case '\n':
            case '\r':
                if (ibuf.len < USB_IBUFSZ) {
                    enqueue('\n', &ibuf);
                    ilines++;
                    CDC_Device_SendString(&serialDevice, "\r\n");
                } else {
                    SET_MASK_HI(input.flags, __SERR);
                }
                break;
            // Backspace/DEL - roll back the input buffer by one, but not if
            // empty or if the last char was a '\n'.  Echo "\b \b".
            case '\b':
            case DEL:
                if (ibuf.len > 0) {
                    // Back up 1 char, with wraparound.
                    char *p = ibuf.head - 1;
                    if (p < ibuf.start) {
                        p = ibuf.end;
                    }
                    // Only erase the character if it isn't a '\n'.
                    if (*p != '\n') {
                        ibuf.head = p;
                        ibuf.len--;
                        CDC_Device_SendString(&serialDevice, "\b \b");
                    }
                }
                break;
            // Save and echo the character if there is space (leave 1 for '\n').
            default:
                if (ibuf.len < USB_IBUFSZ - 1) {
                    enqueue(c, &ibuf);
                    CDC_Device_SendByte(&serialDevice, c);
                } else {
                    SET_MASK_HI(input.flags, __SERR);
                }
                break;
            }
        }

    // Binary mode - no echoing or editing.
    } else {
        while (avail--) {
            char c = CDC_Device_ReceiveByte(&serialDevice);
            if (ibuf.len < USB_IBUFSZ) {
                // Save the character, update the line count if necessary.
                enqueue(c, &ibuf);
                if (c == '\n') {
                    ilines++;
                }
            } else {
                SET_MASK_HI(input.flags, __SERR);
            }
        }
    }

    // Run again in 1 msec, as there may be more data coming.
    nextRun = now + 1;
}

// Save a character into the output stream buffer.  Checks line state, blocks
// if necessary.
static int iobuf_putchar(char c, FILE *file)
{
    // Line must be up.
    if (state != UP) {
        SET_MASK_HI(input.flags, __SEOF);
        return EOF;
    }

    // CRLF handling.
    if (mode & USB_SERIAL_OUT_TEXT && c == '\n') {
        if (CDC_Device_SendByte(&serialDevice, '\r')
          != ENDPOINT_READYWAIT_NoError) {
            SET_MASK_HI(input.flags, __SERR);
            return EOF;
        }
    }

    // Output the character.
    if (CDC_Device_SendByte(&serialDevice, c) != ENDPOINT_READYWAIT_NoError) {
        SET_MASK_HI(input.flags, __SERR);
        return EOF;
    }

    return c;
}

// Get a character from the input stream buffer.  Blocks if necessary.
static int iobuf_getchar(FILE *file)
{
    if (state != UP) {
        SET_MASK_HI(output.flags, __SEOF);
        return EOF;
    }

    // Text mode - wait until a whole line is available.
    char c;
    if (mode & USB_SERIAL_IN_TEXT) {
        while (ilines == 0) {
            run(millis());
        }

    // Binary mode - wait until a character is available.
    } else {
        while (ibuf.len == 0) {
            run(millis());
        }
    }

    // Get the next character.
    c = dequeue(&ibuf);
    if (c == '\n') {
        ilines--;
    }
    return c;
}

/*
 * Public API.
 */

// Initialise the virtual serial line using the two streams.
void usb_serial_init(uint8_t iomode, FILE **in, FILE **out) {
    // Initialise avr-libc streams.
    mode = iomode;
    *in  = &input;
    *out = &output;

    // Initialise LUFA.
    USB_Init();
}

// Initialise the virtual serial line using stdio streams.
void usb_serial_init_stdio(uint8_t iomode) {
    usb_serial_init(iomode, &stdin, &stdout);
    stderr = stdout;
}

// Get the number of input characters available.
int usb_serial_input_chars(void)
{
    return ibuf.len;
}

// Get the number of input lines available.
int usb_serial_input_lines(void)
{
    return ilines;
}

// Run the LUFA USB tasks, if required.
bool usb_serial_run(uint32_t now)
{
    if (now >= nextRun) {
        run(now);
        return true;
    }
    return false;
}

